/*
 * Copyright 2013 Nicolas Morel
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.kie.j2cl.tools.xml.mapper.api.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.kie.j2cl.tools.xml.mapper.api.GwtIncompatible;
import org.kie.j2cl.tools.xml.mapper.api.MapperContext;
import org.kie.j2cl.tools.xml.mapper.api.XMLSerializerParameters;

/**
 * DefaultDateFormat class.
 *
 * @author Nicolas Morel
 * @version $Id: $
 */
@GwtIncompatible
public final class DefaultDateFormat implements MapperContext.DateFormat {

  /**
   * Defines a commonly used date format that conforms to ISO-8601 date formatting standard, when it
   * includes basic undecorated timezone definition
   */
  public static final DateTimeFormatter DATE_FORMAT_STR_ISO8601 =
      DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

  /** Same as 'regular' 8601, but handles 'Z' as an alias for "+0000" (or "GMT") */
  public static final DateTimeFormatter DATE_FORMAT_STR_ISO8601_Z =
      DateTimeFormatter.ISO_ZONED_DATE_TIME;

  /** ISO-8601 with just the Date part, no time */
  public static final DateTimeFormatter DATE_FORMAT_STR_PLAIN =
      DateTimeFormatter.ofPattern("yyyy-MM-dd");

  /** This constant defines the date format specified by RFC 1123 / RFC 822. */
  public static final DateTimeFormatter DATE_FORMAT_STR_RFC1123 =
      DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz");

  /** UTC TimeZone */
  public static final ZoneId UTC_TIMEZONE = ZoneOffset.UTC;

  private static final Map<String, DateParser> CACHE_PARSERS = new HashMap<String, DateParser>();

  /** Constructor for DefaultDateFormat. */
  public DefaultDateFormat() {}

  /**
   * {@inheritDoc}
   *
   * <p>Format a date using {@link #DATE_FORMAT_STR_ISO8601} and {@link #UTC_TIMEZONE}
   */
  public String format(Date date) {
    return format(
        DefaultDateFormat.DATE_FORMAT_STR_ISO8601_Z, DefaultDateFormat.UTC_TIMEZONE, date);
  }

  /**
   * {@inheritDoc}
   *
   * <p>Format a date using {@link XMLSerializerParameters} or default values : {@link
   * #DATE_FORMAT_STR_ISO8601} and {@link #UTC_TIMEZONE}
   */
  public String format(XMLSerializerParameters params, Date date) {
    DateTimeFormatter format;
    if (null == params.getPattern()) {
      format = DefaultDateFormat.DATE_FORMAT_STR_ISO8601_Z;
    } else {
      format = DateTimeFormatter.ofPattern(params.getPattern());
    }

    ZoneId timeZone;
    if (null == params.getTimezone()) {
      timeZone = DefaultDateFormat.UTC_TIMEZONE;
    } else {
      timeZone = (ZoneId) params.getTimezone();
    }

    return format(format, timeZone, date);
  }

  /**
   * {@inheritDoc}
   *
   * <p>Parse a date using the pattern given in parameter or {@link #DATE_FORMAT_STR_ISO8601} and
   * the browser timezone.
   */
  public Date parse(boolean useBrowserTimezone, String pattern, Boolean hasTz, String date) {
    if (null == pattern) {
      try {
        return parse(DefaultDateFormat.DATE_FORMAT_STR_ISO8601, date);
      } catch (DateTimeParseException e) {
        return parse(DefaultDateFormat.DATE_FORMAT_STR_ISO8601_Z, date);
      }
    } else {
      String patternCacheKey = pattern + useBrowserTimezone;
      DateParser parser = CACHE_PARSERS.get(patternCacheKey);
      if (null == parser) {
        boolean patternHasTz =
            useBrowserTimezone || (hasTz == null ? hasTz(pattern) : hasTz.booleanValue());
        if (patternHasTz) {
          parser = new DateParser(pattern);
        } else {
          // the pattern does not have a timezone, we use the UTC timezone as reference
          parser = new DateParserNoTz(pattern);
        }
        CACHE_PARSERS.put(patternCacheKey, parser);
      }
      return parser.parse(date);
    }
  }

  /**
   * Find if a pattern contains informations about the timezone.
   *
   * @param pattern pattern
   * @return true if the pattern contains informations about the timezone, false otherwise
   */
  private boolean hasTz(String pattern) {
    boolean inQuote = false;

    for (int i = 0; i < pattern.length(); i++) {
      char ch = pattern.charAt(i);

      // If inside quote, except two quote connected, just copy or exit.
      if (inQuote) {
        if (ch == '\'') {
          if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') {
            // Quote appeared twice continuously, interpret as one quote.
            ++i;
          } else {
            inQuote = false;
          }
        }
        continue;
      }

      // Outside quote now.
      if ("Zzv".indexOf(ch) >= 0) {
        return true;
      }

      // Two consecutive quotes is a quote literal, inside or outside of quotes.
      if (ch == '\'') {
        if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') {
          i++;
        } else {
          inQuote = true;
        }
      }
    }

    return false;
  }

  /**
   * Format a date using the {@link DateTimeFormatter} and {@link ZoneId} given in parameters
   *
   * @param format format to use
   * @param timeZone timezone to use
   * @param date date to format
   * @return the formatted date
   */
  public String format(DateTimeFormatter format, ZoneId timeZone, Date date) {
    if (date instanceof java.sql.Date || date instanceof java.sql.Time)
      return format.withZone(timeZone).format(new Date(date.getTime()).toInstant());
    return format.withZone(timeZone).format(date.toInstant());
  }

  /**
   * {@inheritDoc}
   *
   * <p>Format a date using the {@link DateTimeFormatter} given in parameter and {@link
   * #UTC_TIMEZONE}.
   */
  public String format(DateTimeFormatter format, Date date) {
    return format(format, UTC_TIMEZONE, date);
  }

  /**
   * {@inheritDoc}
   *
   * <p>Format a date using {@link #DATE_FORMAT_STR_ISO8601} and {@link ZoneId} given in parameter
   */
  public String format(ZoneId timeZone, Date date) {
    return DefaultDateFormat.DATE_FORMAT_STR_ISO8601.withZone(timeZone).format(date.toInstant());
  }

  /**
   * Parse a date using {@link #DATE_FORMAT_STR_ISO8601} and the browser timezone.
   *
   * @param date date to parse
   * @return the parsed date
   */
  public Date parse(String date) {
    return parse(DefaultDateFormat.DATE_FORMAT_STR_ISO8601, date);
  }

  /**
   * Parse a date using the {@link DateTimeFormatter} given in parameter and the browser timezone.
   *
   * @param format format to use
   * @param date date to parse
   * @return the parsed date
   */
  public Date parse(DateTimeFormatter format, String date) {
    return Date.from(Instant.from(format.parse(date)));
  }

  /**
   * Parse a date using the {@link DateTimeFormatter} given in parameter and the browser timezone.
   *
   * @param format format to use
   * @param date date to parse
   * @return the parsed date
   */
  public Date parse(SimpleDateFormat format, String date) {
    try {
      return format.parse(date);
    } catch (ParseException e) {
      return null;
    }
  }

  private class DateParser {

    protected final SimpleDateFormat dateTimeFormat;

    protected DateParser(String pattern) {
      this.dateTimeFormat = new SimpleDateFormat(pattern);
    }

    protected Date parse(String date) {
      return DefaultDateFormat.this.parse(dateTimeFormat, date);
    }
  }

  private class DateParserNoTz extends DateParser {

    protected DateParserNoTz(String pattern) {
      super(pattern + " Z");
    }

    @Override
    protected Date parse(String date) {
      return super.parse(date + " +0000");
    }
  }
}
